标准库(二):JSON 与 HTTP 服务
需要预先了解:
1. 标准库(二):JSON 与 HTTP 服务
encoding/json 和 net/http 是写服务端程序的两块基石。这一篇从"结构体怎么变成 JSON"讲到"一个带中间件、超时和优雅关闭的迷你 REST 服务",把 反射与 unsafe 里看到的 tag 机制落到实际用法上。
1.1 encoding/json 基础
两个函数覆盖 90% 的场景:
b, err := json.Marshal(v) // 值 -> JSON 字节切片
err = json.Unmarshal(data, &v) // JSON -> 值(必须传指针)
u := User{Name: "一撮灰", Score: 98.5, Created: time.Date(2026, 9, 9, 12, 0, 0, 0, time.UTC)}
b, _ := json.Marshal(u)
fmt.Println(string(b))
{"name":"一撮灰","score":"98.5","created":"2026-09-09T12:00:00Z","address":{"city":"上海"}}
需要带缩进的输出用 json.MarshalIndent(v, "", " ")。
Unmarshal 有几个行为需要记住:
- 字段名匹配不区分大小写:JSON 里的
"AGE"也能填到Age字段; - 未知字段被忽略(想报错要用
Decoder.DisallowUnknownFields); - JSON 里缺失的字段保持零值。
var u2 User
json.Unmarshal([]byte(`{"name":"灰灰","AGE":3,"score":"88.5","address":{"city":"北京"}}`), &u2)
fmt.Printf("%+v\n", u2)
{Name:灰灰 Age:3 ... Score:88.5 ... Address:{City:北京}}
1.2 字段名与 tag
默认的字段名就是 Go 字段名(Name 输出为 "Name"),这通常不是我们想要的。用 json tag 指定:
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
结构体与指针 里只讲了 tag 的语法,反射与 unsafe 里讲了 StructTag.Get 怎么解析它——encoding/json 就是在运行时读这些 tag 来决定字段名的。
1.3 tag 进阶
| 写法 | 含义 |
|---|---|
json:"name" |
改名为 name |
json:"-" |
完全忽略该字段 |
json:"age,omitempty" |
零值时省略(数值 0、空串、nil 切片等) |
json:"score,string" |
以字符串形式编码数值/布尔 |
json:",omitempty" |
只加选项,不改名 |
实测效果:
type User struct {
Name string `json:"name"`
Age int `json:"age,omitempty"` // 0 被省略
Email string `json:"-"` // 完全不出现在 JSON 里
Password string `json:"password,omitempty"` // 空串被省略
Score float64 `json:"score,string"` // 数字变字符串
Created time.Time `json:"created"`
Address Address `json:"address"`
Tags []string `json:"tags,omitempty"`
}
{"name":"一撮灰","score":"98.5","created":"2026-09-09T12:00:00Z","address":{"city":"上海"}}
omitempty 只对零值生效。想把 0 也保留下来,就不能加这个选项——这是前后端对接时常见的分歧点。
嵌套结构体会递归序列化。匿名嵌入的字段会被"提升"到外层:
type Base struct {
ID int `json:"id"`
}
type Post struct {
Base
Title string `json:"title"`
}
Post 序列化的结果是 {"id":1,"title":"..."},ID 平铺在外层。如果嵌入字段和外层字段同名,外层优先。
nil 切片与空切片
json.Marshal(map[string]any{
"list": []int{1, 2, 3},
"empty": []int{},
"nil": []int(nil),
})
{"empty":[],"list":[1,2,3],"nil":null}
前端期望数组时,nil 会变成 null 导致解析报错——接口返回列表时记得初始化为 []T{}(见 数组与切片)。
1.4 自定义序列化
实现 json.Marshaler / json.Unmarshaler 接口就能接管某个类型的编解码:
type Status int
func (s Status) MarshalJSON() ([]byte, error) {
switch s {
case StatusActive:
return json.Marshal("active")
case StatusBanned:
return json.Marshal("banned")
default:
return json.Marshal("unknown")
}
}
func (s *Status) UnmarshalJSON(data []byte) error {
var str string
if err := json.Unmarshal(data, &str); err != nil {
return err
}
// ...
return nil
}
a := Account{ID: 1, Status: StatusActive}
b, _ := json.Marshal(a)
fmt.Println(string(b)) // {"id":1,"status":"active"}
注意 UnmarshalJSON 必须用指针接收者,否则无法修改值。
json.RawMessage 延迟解析
有时候 JSON 里某个字段的具体类型取决于另一个字段:
var raw struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
json.Unmarshal([]byte(`{"type":"user","data":{"name":"延迟解析"}}`), &raw)
RawMessage 就是原始的字节切片,等到知道 Type 之后再解析 Data。
1.5 流式 API
Marshal/Unmarshal 处理的是整块字节。数据来自网络或文件时,用 Encoder/Decoder 直接接 io.Writer/io.Reader:
dec := json.NewDecoder(r)
for {
var acc Account
if err := dec.Decode(&acc); err != nil {
break // io.EOF 表示结束
}
fmt.Println(acc)
}
Decoder 可以从一个连接里连续解出多个 JSON 值,这在长连接和流式日志场景里很有用。Encoder.Encode 会自动追加换行。
1.6 net/http 服务端
最小的服务:
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello")
})
http.ListenAndServe(":8080", nil)
http.Handler 是一个只有一个方法的接口:
type Handler interface {
ServeHTTP(w http.ResponseWriter, r *http.Request)
}
http.HandlerFunc 是一个适配器,让普通函数满足这个接口。自己实现 Handler 就是定义一个带 ServeHTTP 方法的结构体:
type App struct {
store *Store
}
func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 自己路由
}
路由(Go 1.22+)
http.ServeMux 从 1.22 起支持方法和路径参数:
mux := http.NewServeMux()
mux.HandleFunc("GET /users", listUsers)
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("GET /users/{id}", getUser)
路径参数用 r.PathValue("id") 取。模式里 {id} 匹配单个路径段,{path...} 匹配剩余部分。不写方法时匹配所有方法。
1.7 中间件
中间件的类型是 func(http.Handler) http.Handler,接收一个 Handler 返回一个新的 Handler,形成"洋葱"结构:
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s 耗时 %v", r.Method, r.URL.Path, time.Since(start))
})
}
请求 -> logging -> withJSON -> mux -> handler
响应 <- logging <- withJSON <- mux <- handler
组合多个中间件:
handler := logging(withJSON(mux))
中间件适合放日志、认证、限流、恢复 panic、设置公共响应头等横切逻辑。恢复 panic 的中间件几乎是必需的——一个没被 recover 的 panic 会让整个进程退出(见 错误处理)。
1.8 请求处理
// 读取请求体,限制大小防止恶意请求
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
// 查询参数
q := r.URL.Query().Get("keyword")
// 表单
r.ParseForm()
name := r.FormValue("name")
// 路径参数(1.22+)
id := r.PathValue("id")
// 返回 JSON
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
顺序很重要:先设置 Header,再 WriteHeader,最后写 body。一旦开始写 body,Header 就定下来了。
1.9 服务端的错误处理
HTTP 服务里错误分两类,处理方式不同。
可预期的失败(参数非法、资源不存在)用状态码 + 统一格式的 JSON 响应表达:
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name 不能为空"})
writeJSON(w, http.StatusNotFound, map[string]string{"error": "用户不存在"})
几条惯例:
- 4xx 表示调用方的问题,5xx 表示服务端的问题,不要都用 200 + 错误字段;
- 错误响应的字段名保持一致(比如统一用
error),方便客户端处理; - 不要把内部错误(SQL 语句、文件路径、堆栈)直接返回给客户端,记录到日志即可;
- 内部错误用
%w包装后向上传递,在最外层统一转换成响应(错误包装的用法见 错误处理)。
不可预期的 panic 必须在中间件里兜底,否则一个请求触发的 panic 会让整个进程退出:
func recoverer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic: %v\n%s", err, debug.Stack())
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "服务器内部错误"})
}
}()
next.ServeHTTP(w, r)
})
}
recover 只能拦住同一个 goroutine 的 panic,所以这个中间件要放在最外层(见 错误处理)。
1.10 超时与优雅关闭
http.Server 的零值没有超时,慢连接会一直占着 goroutine。生产环境要显式设置:
srv := &http.Server{
Addr: ":8080",
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
优雅关闭:收到信号后停止接收新请求,等待正在处理的请求完成,再退出:
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal(err)
}
Shutdown 返回后,所有活跃连接都已结束(或超过 5 秒被强制中断)。这一步在容器环境里尤其重要——Kubernetes 滚动更新时会给进程发 SIGTERM,不给它机会处理完在途请求就会丢数据。
1.11 客户端
client := &http.Client{Timeout: 3 * time.Second}
resp, err := client.Get("https://example.com")
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
两点注意:
resp.Body必须关闭,否则连接无法复用,会泄漏文件描述符;Client.Timeout与context的区别:Client.Timeout覆盖整个请求(连接、重定向、读 body),context更灵活,可以只限制某一阶段,也能在业务取消时立即中断:
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
resp, err := client.Do(req)
默认的 http.Client 会复用连接(keep-alive)。需要精细控制连接池时调整 Transport 的 MaxIdleConnsPerHost 等参数。
1.12 实战:迷你 REST 服务
把上面的内容拼起来,一个约 120 行的服务:
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
type Store struct {
mu sync.RWMutex
users map[int]User
nextID int
}
func NewStore() *Store {
return &Store{users: map[int]User{}, nextID: 1}
}
func (s *Store) List() []User {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]User, 0, len(s.users)) // 初始化为空切片,避免输出 null
for _, u := range s.users {
out = append(out, u)
}
return out
}
func (s *Store) Get(id int) (User, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
u, ok := s.users[id]
return u, ok
}
func (s *Store) Create(name string) User {
s.mu.Lock()
defer s.mu.Unlock()
u := User{ID: s.nextID, Name: name}
s.users[u.ID] = u
s.nextID++
return u
}
路由与处理器:
mux := http.NewServeMux()
mux.HandleFunc("GET /users", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, store.List())
})
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
var id int
fmt.Sscanf(r.PathValue("id"), "%d", &id)
u, ok := store.Get(id)
if !ok {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "用户不存在"})
return
}
writeJSON(w, http.StatusOK, u)
})
mux.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
var req struct {
Name string `json:"name"`
}
if err := json.Unmarshal(body, &req); err != nil || req.Name == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name 不能为空"})
return
}
writeJSON(w, http.StatusCreated, store.Create(req.Name))
})
handler := logging(withJSON(mux))
启动、测试、优雅关闭:
srv := &http.Server{
Addr: "127.0.0.1:18080",
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
用 http.Client 实测输出:
POST 状态: 201 响应: {"id":1,"name":"一撮灰"}
GET /users: [{"id":1,"name":"一撮灰"},{"id":2,"name":"灰灰"}]
GET /users/1: 200 {"id":1,"name":"一撮灰"}
GET /users/999: 404 {"error":"用户不存在"}
带 ctx 的请求状态: 200
服务已优雅关闭
这个骨架里包含了并发安全的存储(RWMutex)、中间件、路径参数、JSON 编解码、请求体限流、超时和优雅关闭——生产服务的结构基本就是这样。
1.13 练习
- 给迷你服务加上
PUT /users/{id}和DELETE /users/{id},注意处理"用户不存在"的返回码。 - 加一个
recover中间件:在处理器里故意 panic,验证请求返回 500 而不是整个进程退出。 - 用
json.Decoder+DisallowUnknownFields改造POST /users,让请求里出现未知字段时返回 400。



